跳到主要内容

Java代理模式

代理模式

proxy.png

常用的代理又分为 动态代理静态代理

proxy.jpg

静态代理

静态代理在使用时,需要定义接口或者父类,被代理对象一起实现相同接口或者继承相同父类

而静态代理又分两类 继承聚合

继承实现静态代理

就是利用了继承。然后覆写那个方法,实例化时去实例这个静态代理就行了,与反射机制无关

public class UserDaoImpl{
public void login(String name){
System.out.println(name + "login 成功");
}
}
/*----------------代理类-------------------*/
public class UserLogProxy extends UserDaoImpl{
@Override
public void login(String name){
System.out.println("这里是代理内容");
//调用父类的内容
super.login(name);
}
}

聚合实现静态代理

与继承的那个差不多,只不过多了一层接口

public interface UserDao{
public void login(String name);
}
/*-----------------------业务类------------------------------------*/
public class UserDaoImpl implements UserDao{
public void login(String name){
System.out.println(name + "login 成功");
}
}
/*-------------------------代理类-----------------------------------*/
public class UserLogProxy implements UserDao{
private UserDao target;
//构造函数里把这个目标对象传进来
public UserLogProxy(UserDao target){
this.target = target;
}

@Override
public void login(String name){
System.out.println("这里是代理内容");
//调用目标对象的方法
target.login(name);
}
}

使用聚合静态代理时通过这个共同的接口去接受这个代理类就好了

UserDao a = new UserLogProxy(name);

静态代理的缺点

使用静态代理也有缺点,就是当接口修改了(或者父类修改了),整个代码都需要修改,这样就破坏了开闭原则,且多了很多重复的代码(比如实现一个代理对象就要再一个接口和一个代理对象的文件)。所以需要引入动态代理,让这一切自动完成

动态代理

相比于静态代理来说,动态代理更加灵活。我们不需要针对每个目标类都单独创建一个代理类,并且也不需要我们必须实现接口,我们可以直接代理实现类(CGLIB 动态代理机制)。

从 JVM 角度来说,动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。

说到动态代理,Spring AOP、RPC 框架应该是两个不得不的提的,它们的实现都依赖了动态代理。

动态代理在我们日常开发中使用的相对较小,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助。

就 Java 来说,动态代理的实现方式有很多种,比如 JDK 动态代理、CGLIB 动态代理等等。

JDK 的动态代理

Proxy 工具类的使用

public class Temp {
public static void main(String[] args) {
Human human = new Everyman();
human.info("张三");
human.walk();

System.out.println("======================================================");

// 生成代理对象
Human proxyInstance = (Human)Proxy.newProxyInstance(
human.getClass().getClassLoader(),
human.getClass().getInterfaces(),
(Object proxy, Method method, Object[] parameter) -> {

if ("info".equals(method.getName())) {
// 这里直接调用了上面创建的实例对象,一般来说是创建一个 InvocationHandler 实现类,通过构造方法传进来的
method.invoke(human, parameter);
System.out.println("现在我被代理了,所以现在我是代理人!我的名字是:" + parameter[0]);
}

if ("walk".equals(method.getName())) {
method.invoke(human, parameter);
System.out.println("现在我被代理了,所以现在我能跑 200米不带喘!");
}

return proxy;
});

proxyInstance.info("李四");
proxyInstance.walk();
}

// 还是需要先定义一个接口
interface Human {
void info(String name);
void walk();
}

static class Everyman implements Human{
@Override
public void info(String name) {
System.out.println("我是个普通人,我的名字是:" + name);
}

@Override
public void walk() {
System.out.println("我能跑 50米不带喘");
}
}
}

输出为:

我是个普通人,我的名字是:张三
我能跑 50米不带喘
======================================================
我是个普通人,我的名字是:李四
现在我被代理了,所以现在我是代理人!我的名字是:李四
我能跑 50米不带喘
现在我被代理了,所以现在我能跑 200米不带喘!

一般是单独实现 InvocationHandler 这个接口,这样可以使用构造方法,把被代理对象传进来,而不是上面例子那样直接把实例对象丢到匿名内部类里面执行

动态代理生成的 $Proxy0

至于动态实现业务代码怎么实现的,可以通过查看丢到内存的那块字节码(通过工具保存到本地再反编译)来查看源文件

package com.sun.proxy;

import com.test.spring.proxy.jdk.IHello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

// 可以看到父类是Proxy
public final class $Proxy0 extends Proxy
implements IHello // 和实现了自定义的 IHello接口
{
// 变量,都是 private static Method XXX
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;

// 静态代码块对变量进行一些初始化工作
static {
try {
// 这里每个方法对象 和类的实际方法绑定
m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[] { Class.forName("java.lang.Object") });
m3 = Class.forName("com.jpeony.spring.proxy.jdk.IHello").getMethod("sayHello", new Class[0]);
m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
return;
}
catch (NoSuchMethodException localNoSuchMethodException) {
throw new NoSuchMethodError(localNoSuchMethodException.getMessage());
}
catch (ClassNotFoundException localClassNotFoundException) {
throw new NoClassDefFoundError(localClassNotFoundException.getMessage());
}
}
}

// 代理类的构造函数,其参数正是是InvocationHandler实例,Proxy.newInstance方法就是通过通过这个构造函数来创建代理实例的
public $Proxy0(InvocationHandler paramInvocationHandler)
throws
{
super(paramInvocationHandler);
//这个父类执行的操作
//protected Proxy(InvocationHandler h) {
//Objects.requireNonNull(h);
//this.h = h;
//}

//注意!这个 h 是Proxy这个父类定义的"protected InvocationHandler h;"
}

// Object中的三个方法也有,但是篇幅限制就省略掉了(equals,toString,hashCode)

// 接口代理方法
public final void sayHello()
throws
{
try {
// 实际上还是通过传入原方法的 Method来反射执行
this.h.invoke(this, m3, null);
return;
}
catch (RuntimeException localRuntimeException) {
throw localRuntimeException;
}
catch (Throwable localThrowable) {
throw new UndeclaredThrowableException(localThrowable);
}
}

可以发现实际上,在调用代码时是转发给 InvocationHandler 里面的 invoke 方法,然后在 invoke 方法通过反射来实现原函数的业务代码

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法开始执行...");
//通过反射的方法调用方法
Object result = method.invoke(target, args);
System.out.println(method.getName() + "方法执行结束...");
return result;
}

newProxyInstance 简单实现

通过上面那个生成的 $Proxy0 文件可知,JDK 的动态代理原理与上面的那个静态代理之聚合基本一致,只是它不用手动去创建一个,而是通过程序拼接生成一个 Java文件再动态编译

这里自己实现一个简易版的动态代理,因为无需复制的变化就直接把代理的方法写死了

具体的各个方法实现看下面

public static Object newProxyInstance(Object target) {
// 生成 Java文件
String content = makeJavaFile(target);
// 编译 Java文件
compilerJavaFile(content);
// 返回生成的代理对象
return loadClassObject("", "$Proxy", target);
}

在编写这个 newProxyInstance 之前先来看下 URLClassLoader 的使用,使用它可以动态加载 class 文件

File file = new File(""); // 这里无需指定哪个 class 文件,只需一个路径就行了,例如这里默认当前执行路径
try {
URL url = file.toURI().toURL(); // 直接使用自带的工具方法
System.out.println(url); // file:/D:/JavaProject/studyALG 注意本地路径需要前面加 file:/

URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
// 注意这个取得类还是要全类名,但是因为路径的问题这里没有使用包路径(需要映射物理路径的)
Class cls = urlClassLoader.loadClass("$Proxy");
} catch (Exception e) {
e.printStackTrace();
}

JavaCompiler 是用于动态编译工具类

//动态编译生成的 java 文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int flag = compiler.run(null, null, null, "$Proxy.java");
System.out.println(flag == 0 ? "编译成功" : "编译失败");

下面分成几个模块来组成这个 newProxyInstance 方法

生成 Java 文件

先生成一个 Java 文件用于后面的编译

/**
* 生成一个 Java文件
* @param target 被代理的对象
* @return Java文件内容
*/
private static String makeJavaFile(Object target) {
String line = "\n";
Class targetInf = target.getClass().getInterfaces()[0];
// String packageNameContent = "package " + "com.alsritter" + ";" + line; // 这里不写包名了
String importContent = "import " + targetInf.getName() + ";" + line; // 取得被代理对象的包名
String infoName = targetInf.getSimpleName(); // 取得接口的简写名称

/*
拼接类的定义,为了避免冲突,源码是使用了计步器 long num = nextUniqueNumber.getAndIncrement();
形如 $Proxy0, $Proxy1, $Proxy2 这样
这里直接写死
*/
String clazzFirstLineContent = "public class $Proxy implements " + infoName + "{" + line;
String fieldContent = "private " + infoName + " target;" + line; // 拼接一个目标对象

//拼接一个构造方法
String constructorContent = line +
"public $Proxy(" + infoName + " target){" + line +
"this.target = target;" + line +
"}" + line;

// 取得被代理的方法
StringBuilder methodsContent = new StringBuilder();
Method[] methods = targetInf.getDeclaredMethods();
for (Method method : methods) {
String methodName = method.getName();
String returnTypeName = method.getReturnType().getSimpleName();
Class[] args = method.getParameterTypes();
//拼接参数
StringBuilder argsContent = new StringBuilder();
StringBuilder paramsContent = new StringBuilder();
int i = 0;
for (Class arg : args) {
String simpleName = arg.getSimpleName();
argsContent.append(simpleName).append(" p").append(i).append(",");
//因为上面那个包括了参数类型名,所以要另创建一个用来存拼接参数
paramsContent.append("p").append(i).append(",");
i++;
}

//要把最后一个“,”删掉
if (argsContent.length() > 0) {
argsContent = new StringBuilder(argsContent.substring(0, argsContent.lastIndexOf(",")));
paramsContent = new StringBuilder(paramsContent.substring(0, paramsContent.lastIndexOf(",")));
}

// 拼接方法,这里直接把方法内容写死(注意,如果是 void 的方法不需要 return)
methodsContent.append(line)
.append("public ").append(returnTypeName).append(" ").append(methodName).append("(").append(argsContent).append("){")
.append(line).append("System.out.println(\"这里是代理内容\");")
.append(returnTypeName.equals("void") ? "" : "return ")
.append("target.").append(methodName)
.append("(").append(paramsContent).append(");").append(line)
.append("}").append(line);
}

// 最后把所有内容拼在一起
return importContent
+ clazzFirstLineContent + fieldContent
+ constructorContent + methodsContent + "}";
}

动态编译 Java文件

先把上面生成的 Java字符串保存到本地文件,再调用 JavaCompiler 编译

/**
* 编译 Java文件
* 源码里是把文件直接存在字节码(byte[] proxyClassFile)里面,直接在内存里编译
* 这里为了方便查看生成的 Java 文件,所以把这个拼接类保存为 java 文件
*
* @param content 传入的字符串
*/
private static void compilerJavaFile(String content) {
File file = new File("$Proxy.java");
if (!file.exists()) {
try {
// 如果文件不存在则创建一个空的文件
file.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
try (FileWriter fw = new FileWriter(file)) {
fw.write(content);
//清空缓冲区并完成文件写入操作
fw.flush();
} catch (IOException e) {
e.printStackTrace();
}

//动态编译生成的java文件
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int flag = compiler.run(null, null, null, "$Proxy.java");
System.out.println(flag == 0 ? "编译成功" : "编译失败");
}

class 文件加载进 JVM

/**
* 生成了编译好的字节码文件后需要将它加载到 JVM 里面
* 所以这里使用 URLClassLoader 这个加载器
* 使用 URLClassLoader 加载 Class,除了可以加载网络的 class 还可以加载本地 class(注意这个路径问题)
* 注意,这里无需写明具体的 $Proxy.class 路径,因为 urlClassLoader 加载的是目录下的 class 文件,所以只需目录
*
* @param path class 文件的目录
* @param filename 需要被加载的 class 文件名(类名)
* @param target 被代理的对象
* @return 返回生成的代理对象
*/
private static Object loadClassObject(String path, String filename, Object target) {
File clazzPath = new File(path);
Object object = null;
URL urls = null;
try {
urls = clazzPath.toURI().toURL();
} catch (MalformedURLException e) {
e.printStackTrace();
}

System.out.println(urls); // file:/D:/JavaProject/studyALG/$Proxy.class

try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{urls})) {
// 与上面写的包名对应需要输入全类名
Class<?> loadClass = urlClassLoader.loadClass(filename);
object = loadClass.getConstructor(target.getClass().getInterfaces()[0]).newInstance(target);
} catch
(Exception e) {
e.printStackTrace();
}
return object;
}

编写测试

先声明一个共有的接口

public interface TestInterface {
void sayHello();
}

编写被代理的对象,以及使用 newProxyInstance 方法生成代理(直接在这里编写 main 方法了)

public class TestObject implements TestInterface {
@Override
public void sayHello() {
System.out.println("你好,这里是 TestObject");
}

public static void main(String[] args) {
TestInterface testObject = new TestObject();

TestInterface proxyInstance = (TestInterface) Temp.newProxyInstance(testObject);
proxyInstance.sayHello();

}
}

输出内容为:

编译成功
这里是代理内容
你好,这里是 TestObject

CGLIB 动态代理机制

参考资料 代理模式详解

JDK 动态代理有一个最致命的问题是其 只能代理实现了接口的类

为了解决这个问题,我们可以用 CGLIB 动态代理机制来避免。

CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。CGLIB 通过继承方式实现代理。很多知名的开源框架都使用到了CGLIB, 例如 Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理

在 CGLIB 动态代理机制中 MethodInterceptor 接口和 Enhancer 类是核心。

需要自定义 MethodInterceptor 并重写 intercept 方法,intercept 用于拦截增强被代理类的方法。

public interface MethodInterceptor
extends Callback{
// 拦截被代理类中的方法
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable;
}
  • obj :被代理的对象(需要增强的对象)
  • method :被拦截的方法(需要增强的方法)
  • args :方法入参
  • methodProxy :用于调用原始方法

可以通过 Enhancer 类来动态获取被代理类,当代理类调用方法的时候,实际调用的是 MethodInterceptor 中的 intercept 方法。

CGLIB 动态代理类使用步骤

因为 CGLIB 是属于一个开源项目,如果要使用它的话,需要手动添加相关依赖。

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

实现一个使用阿里云发送短信的类

public class AliSmsService {
public String send(String message) {
System.out.println("send message:" + message);
return message;
}
}

自定义 MethodInterceptor,在这里实现 MethodInterceptor接口(方法拦截器)

/**
* 自定义MethodInterceptor
*/
public class DebugMethodInterceptor implements MethodInterceptor {


/**
* @param o 被代理的对象(需要增强的对象)
* @param method 被拦截的方法(需要增强的方法)
* @param args 方法入参
* @param methodProxy 用于调用原始方法
*/
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
//调用方法之前,我们可以添加自己的操作
System.out.println("before method " + method.getName());
Object object = methodProxy.invokeSuper(o, args);
//调用方法之后,我们同样可以添加自己的操作
System.out.println("after method " + method.getName());
return object;
}

}

封装获取代理类的工具类

public class CglibProxyFactory {

public static Object getProxy(Class<?> clazz) {
// 创建动态代理增强类
Enhancer enhancer = new Enhancer();
// 设置类加载器
enhancer.setClassLoader(clazz.getClassLoader());
// 设置被代理类
enhancer.setSuperclass(clazz);
// 设置方法拦截器
enhancer.setCallback(new DebugMethodInterceptor());
// 创建代理类
return enhancer.create();
}
}

实际使用

AliSmsService aliSmsService = (AliSmsService) CglibProxyFactory.getProxy(AliSmsService.class);
aliSmsService.send("java");

运行上述代码之后,控制台打印出:

before method send
send message:java
after method send

JDK 动态代理和 CGLIB 对比

JDK 动态代理只能只能代理实现了接口的类,而 CGLIB 可以代理未实现任何接口的类。另外,CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。

就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。

静态代理和动态代理的对比

灵活性 :动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!

JVM 层面 :静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码(加载到内存),并加载到 JVM 中的。